這篇章會把 gRPC 透過 BFF Gateway 轉換成常見的 REST API,並且使用 .http
快速做測試。
不知道讀者是否有過好奇,怎麼只寫一個 Proto 檔案卻有一堆已經產好的類別可以直接做使用?答案是這個套件會自動幫你產生一些關於這個 Proto 的 CS 檔案,可以隨意開啟一個 gRPC 專案,在 obj\Debug\net8.0
的資料夾內會找到一些蛛絲馬跡,其實我們用到的所有相關類別都會在這幾個檔案內。
所以我們這次的目標,就是利用 Grpc.AspNetCore
來幫 BFF Gateway 產生 Client 的工具。
為了避免管理兩份同樣的 Proto 檔案,我們使用 MSBuild 的 Link
屬性來建立連結,這樣就不需要複製 proto
檔案了,至於正式要 Deploy 到部屬環境的時候該如何處理檔案複製的問題,當然是讓公司 DevOps 來處理(笑),實作如下:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<!-- Link Account and Todo proto files -->
<Protobuf Include="..\..\Account\Account.Grpc\Protos\account.proto" Link="Protos\account.proto" GrpcServices="Client" />
<Protobuf Include="..\..\Todo\Todo.Grpc\Protos\todoList.proto" Link="Protos\todoList.proto" GrpcServices="Client" />
<Protobuf Include="..\..\Todo\Todo.Grpc\Protos\todoItem.proto" Link="Protos\todoItem.proto" GrpcServices="Client" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="Grpc.AspNetCore" Version="2.57.0" />
</ItemGroup>
</Project>
這邊除了 Link 了檔案路徑,還能將 GrpcServices 指定為 Client,這樣 Grpc.Tools
就只會產生客戶端的類別工具,另外還要記得安裝Grpc.AspNetCore
。
都做好之後要 dotnet clean
和 dotnet build
,確保有自動產生相關工具。
新增 Controllers 這件事情我相信會讀到這裡的讀者都很熟悉了,這裡我就不展開太多,這裡我直接拿 Grpc 的 Request 和 Response 來用,這不是一個好作法,自己做的時候記得要建立相關的 DTOs 來做隔離。這裡推薦用 Mapster
為自動化 Mapper 的工具。話不多說,直接上 Code:
AccountController
using Account.Grpc;
using Microsoft.AspNetCore.Mvc;
namespace BFF.Gateway.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AccountController : ControllerBase
{
private readonly AccountGrpcService.AccountGrpcServiceClient _accountClient;
public AccountController(AccountGrpcService.AccountGrpcServiceClient accountClient)
{
_accountClient = accountClient;
}
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] RegisterRequest request)
{
var response = await _accountClient.RegisterAsync(request);
return Ok(response);
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
var response = await _accountClient.LoginAsync(request);
return Ok(response);
}
}
TodoListController
using Todo.Grpc;
using Microsoft.AspNetCore.Mvc;
namespace BFF.Gateway.Controllers;
[ApiController]
[Route("api/[controller]")]
public class TodoListController : ControllerBase
{
private readonly TodoListGrpcService.TodoListGrpcServiceClient _todoListClient;
public TodoListController(TodoListGrpcService.TodoListGrpcServiceClient todoListClient)
{
_todoListClient = todoListClient;
}
[HttpPost("create")]
public async Task<IActionResult> CreateTodoList([FromBody] CreateTodoListRequest request)
{
var response = await _todoListClient.CreateTodoListAsync(request);
return Ok(response);
}
[HttpDelete("remove/{id}")]
public async Task<IActionResult> RemoveTodoList(Guid id)
{
var response = await _todoListClient.RemoveTodoListAsync(new RemoveTodoListRequest() { Id = id.ToString() });
return Ok(response);
}
}
TodoItemController
using Todo.Grpc;
using Microsoft.AspNetCore.Mvc;
namespace BFF.Gateway.Controllers;
[ApiController]
[Route("api/[controller]")]
public class TodoItemController : ControllerBase
{
private readonly TodoItemGrpcService.TodoItemGrpcServiceClient _todoItemClient;
public TodoItemController(TodoItemGrpcService.TodoItemGrpcServiceClient todoItemClient)
{
_todoItemClient = todoItemClient;
}
[HttpPost("create")]
public async Task<IActionResult> CreateTodoItem([FromBody] CreateTodoItemRequest request)
{
var response = await _todoItemClient.CreateTodoItemAsync(request);
return Ok(response);
}
[HttpPost("finish")]
public async Task<IActionResult> FinishTodoItem([FromBody] FinishTodoItemRequest request)
{
var response = await _todoItemClient.FinishTodoItemAsync(request);
return Ok(response);
}
[HttpDelete("remove/{id}")]
public async Task<IActionResult> RemoveTodoItem(Guid id)
{
var response = await _todoItemClient.RemoveTodoItemAsync(new RemoveTodoItemRequest() { Id = id.ToString() });
return Ok(response);
}
}
gRPC 套件有提供 Service Extension 讓我們輕鬆的 DI 我們的 gRPC Client,講白一點就是這套件甚麼都做好了,跟著做即可。
來修改我們的 Program.cs
,把 gRPC Client 都 DI 進去,並加上 Controllers 和 Swagger,整理如下:
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddControllers();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "GrpcRestGateway", Version = "v1" });
});
// gRPC client services for each gRPC service
builder.Services.AddGrpcClient<Account.Grpc.AccountGrpcService.AccountGrpcServiceClient>(o =>
{
o.Address = new Uri("http://localhost:5077");
});
builder.Services.AddGrpcClient<Todo.Grpc.TodoListGrpcService.TodoListGrpcServiceClient>(o =>
{
o.Address = new Uri("http://localhost:5144");
});
builder.Services.AddGrpcClient<Todo.Grpc.TodoItemGrpcService.TodoItemGrpcServiceClient>(o =>
{
o.Address = new Uri("http://localhost:5144");
});
var app = builder.Build();
// Middleware
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "GrpcRestGateway v1"));
}
app.MapControllers();
app.Run();
大家 gRPC Service 的 port 可能不一樣,自己注意一下。
到這邊就成功將 gRPC Endpoint 轉為 REST Endpoint,並且統一由 Gateway 來轉發 Request 了。
先把 Account.Grpc
, Todo.Grpc
, BFF.Gateway
都 dotnet run
起來。
用 Browser 打開 http://localhost:[port]/swagger/index.html
檢查一下 Swagger 有沒有開啟
註冊後再登入看看是否有成功
.http
在我們創建 WebAPI 專案的時候,dotnet 會自動在專案內加入 .http
的測試文件,它可以讓我們使用 REST Client 等套件直接在 IDE(VS Code) 上直接利用文件做測試,Swagger 甚麼的暫時先去旁邊蹲。在這裡,我們可以把 BFF.Gateway.http
改成以下這樣:
@HostAddress = http://localhost:5282
### AccountController - Register
POST {{HostAddress}}/api/Account/register
Content-Type: application/json
{
"firstName": "string",
"lastName": "string",
"email": "string",
"password": "string"
}
### AccountController - Login
POST {{HostAddress}}/api/Account/login
Content-Type: application/json
{
"email": "string",
"password": "string"
}
### TodoItemController - Create Todo Item
POST {{HostAddress}}/api/TodoItem/create
Content-Type: application/json
{
"listId": "string",
"content": "string"
}
### TodoItemController - Finish Todo Item
POST {{HostAddress}}/api/TodoItem/finish
Content-Type: application/json
{
"id": "string"
}
### TodoItemController - Remove Todo Item
DELETE {{HostAddress}}/api/TodoItem/remove/B2D3DB93-6CF2-408A-8360-CFA8AE5AFC88
### TodoListController - Create Todo List
POST {{HostAddress}}/api/TodoList/create
Content-Type: application/json
{
"userId": "string",
"name": "string",
"description": "string"
}
### TodoListController - Remove Todo List
DELETE {{HostAddress}}/api/TodoList/remove/B2D3DB93-6CF2-408A-8360-CFA8AE5AFC88
有了 .http
文件,除了可以快速設置更多參數外,我們也不用在 VS Code 和瀏覽器或 Postman 之間一直切換視窗了。
還記得在 Account Register 和 Account Login 的時候有介紹過 JWT 如何產生,並且放在 Response 回傳出來,下一篇會在 BFF Gateway 中驗證此 JWT 來當作保護 Endpoint 的安全機制。